package top.lichunsheng.minio;

import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import io.minio.*;
import io.minio.errors.*;
import io.minio.http.Method;
import io.minio.messages.*;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.http.MediaTypeFactory;
import top.lichunsheng.config.MinioProperties;
import top.lichunsheng.vo.CreateMultipartUploadResultVO;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.ZonedDateTime;
import java.util.*;

/**
 * @author lcs
 */
@Slf4j
public class MinioTemplate {

    /**
     * MinIO 客户端
     */
    private final PearlMinioClient pearlMinioClient;
    private final MinioProperties minioProperties;

    @Autowired
    public MinioTemplate(PearlMinioClient pearlMinioClient, MinioProperties minioProperties) {
        this.pearlMinioClient = pearlMinioClient;
        this.minioProperties = minioProperties;
    }

    /**
     * 查询所有存储桶
     *
     * @return Bucket 集合
     */
    @SneakyThrows
    public List<Bucket> listBuckets() {
        return pearlMinioClient.listBuckets();
    }

    /**
     * 桶是否存在
     *
     * @param bucketName 桶名
     * @return 是否存在
     */
    @SneakyThrows
    public boolean bucketExists(String bucketName) {
        return pearlMinioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
    }

    /**
     * 创建存储桶
     *
     * @param bucketName 桶名
     */
    @SneakyThrows
    public void makeBucket(String bucketName) {
        if (!bucketExists(bucketName)) {
            pearlMinioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
        }
    }

    /**
     * 删除一个空桶 如果存储桶存在对象不为空时，删除会报错。
     *
     * @param bucketName 桶名
     */
    @SneakyThrows
    public void removeBucket(String bucketName) {
        pearlMinioClient.removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build());
    }


    /**
     * 根据字节上传文件对象
     *
     * @param bytes
     * @param path
     */
    public void putObjectByBytes(byte[] bytes, String path) {
        try {
//           上传文件
            PutObjectArgs build = PutObjectArgs.builder()
                    .bucket(minioProperties.getBucketName())
                    .object(path).stream(new ByteArrayInputStream(bytes), bytes.length, -1)
                    .contentType(MediaTypeFactory.getMediaType(path).orElse(MediaType.APPLICATION_OCTET_STREAM).toString())
                    .build();
            pearlMinioClient.putObject(build);
        } catch (Exception e) {
            log.error("minio上传文件异常： {}  {}", e.getMessage(), e);
        }
    }

    /**
     * 上传文件
     *
     * @param inputStream      流
     * @param originalFileName 原始文件名
     * @param bucketName       桶名
     * @return OssFile
     */
    @SneakyThrows
    public OssFile putObject(InputStream inputStream, String bucketName, String originalFileName) {
        String uuidFileName = generateOssUuidFileName(originalFileName);
        try {
            if (StrUtil.isEmpty(bucketName)) {
                bucketName = minioProperties.getBucketName();
            }
            pearlMinioClient.putObject(
                    PutObjectArgs.builder().bucket(bucketName).object(uuidFileName).stream(
                                    inputStream, inputStream.available(), -1)
                            .build());
            return new OssFile(uuidFileName, originalFileName);
        } finally {
            if (inputStream != null) {
                inputStream.close();
            }
        }
    }

    /**
     * 返回临时带签名、过期时间一天、Get请求方式的访问URL
     *
     * @param bucketName  桶名
     * @param ossFilePath Oss文件路径
     * @return
     */
    @SneakyThrows
    public String getPresignedObjectUrl(String bucketName, String ossFilePath, Map<String, String> queryParams) {
        return pearlMinioClient.getPresignedObjectUrl(
                GetPresignedObjectUrlArgs.builder()
                        .method(Method.PUT)
                        .bucket(bucketName)
                        .object(ossFilePath)
                        .expiry(60 * 60 * 24)
                        .extraQueryParams(queryParams)
                        .build());
    }


    /**
     * GetObject接口用于获取某个文件（Object）。此操作需要对此Object具有读权限。
     *
     * @param bucketName  桶名
     * @param ossFilePath Oss文件路径
     */
    @SneakyThrows
    public InputStream getObject(String bucketName, String ossFilePath) {
        return pearlMinioClient.getObject(
                GetObjectArgs.builder().bucket(bucketName).object(ossFilePath).build());
    }

    /**
     * 批量删除对象文件
     *
     * @param deleteObjects
     */
    @SneakyThrows
    public void removeObjects(List<DeleteObject> deleteObjects) {
        Iterable<io.minio.Result<DeleteError>> results = pearlMinioClient.removeObjects(RemoveObjectsArgs.builder()
                .bucket(minioProperties.getBucketName())
                .objects(deleteObjects)
                .build());
        try {
            for (io.minio.Result<DeleteError> result : results) {
                DeleteError error = result.get();
                log.error("Error in deleting object " + error.objectName() + "; " + error.message());
            }
        } catch (Exception e) {
            log.error("minio 删除文件异常:{}", e.toString());
        }
    }

    /**
     * 查询桶的对象信息
     *
     * @param bucketName 桶名
     * @param recursive  是否递归查询
     * @return
     */
    @SneakyThrows
    public Iterable<Result<Item>> listObjects(String bucketName, boolean recursive) {
        return pearlMinioClient.listObjects(
                ListObjectsArgs.builder().bucket("my-bucketname").recursive(recursive).build());
    }


    public void delByDir(String delDir) {
        try {
            Iterable<io.minio.Result<Item>> results = listObjects(delDir);
            for (io.minio.Result<Item> result : results) {
                boolean dir = result.get().isDir();
                String objectName = result.get().objectName();
                if (dir) {
                    this.delByDir(objectName);
                }
                pearlMinioClient.removeObject(
                        RemoveObjectArgs.builder().bucket(minioProperties.getBucketName()).object(objectName).build());
            }
        } catch (Exception e) {
            log.error("minio删除文件异常 {}  {}", e.getMessage(), e);
        }
    }

    public Iterable<io.minio.Result<Item>> listObjects(String dir) {
        return pearlMinioClient.listObjects(
                ListObjectsArgs.builder().bucket(minioProperties.getBucketName())
                        .prefix(dir)
                        .build());
    }


    /**
     * 获取带签名的临时上传元数据对象，前端可获取后，直接上传到Minio
     *
     * @param bucketName
     * @param fileName
     * @return
     */
    @SneakyThrows
    public Map<String, String> getPresignedPostFormData(String bucketName, String fileName) {
        // 为存储桶创建一个上传策略，过期时间为7天
        PostPolicy policy = new PostPolicy(bucketName, ZonedDateTime.now().plusDays(1));
        // 设置一个参数key，值为上传对象的名称
        policy.addEqualsCondition("key", fileName);
        // 添加Content-Type，例如以"image/"开头，表示只能上传照片，这里吃吃所有
        policy.addStartsWithCondition("Content-Type", MediaType.ALL_VALUE);
        // 设置上传文件的大小 64kiB to 10MiB.
        //policy.addContentLengthRangeCondition(64 * 1024, 10 * 1024 * 1024);
        return pearlMinioClient.getPresignedPostFormData(policy);
    }

    /**
     * 上传分片上传请求，返回uploadId
     */
    public CreateMultipartUploadResponse uploadId(String bucketName, String region, String objectName, Multimap<String, String> headers, Multimap<String, String> extraQueryParams) throws NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException, ServerException, XmlParserException, ErrorResponseException, InternalException, InvalidResponseException {
        return pearlMinioClient.createMultipartUpload(bucketName, region, objectName, headers, extraQueryParams);
    }

    /**
     * 完成分片上传，执行合并文件
     *
     * @param bucketName       存储桶
     * @param region           区域
     * @param objectName       对象名
     * @param uploadId         上传ID
     * @param parts            分片
     * @param extraHeaders     额外消息头
     * @param extraQueryParams 额外查询参数
     */
    private ObjectWriteResponse completeMultipartUpload(String bucketName, String region, String objectName, String uploadId, Part[] parts, Multimap<String, String> extraHeaders, Multimap<String, String> extraQueryParams) throws NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException, ServerException, XmlParserException, ErrorResponseException, InternalException, InvalidResponseException {
        return pearlMinioClient.completeMultipartUpload(bucketName, region, objectName, uploadId, parts, extraHeaders, extraQueryParams);
    }

    public String generateOssUuidFileName(String originalFilename) {
        return "files" + StrUtil.SLASH + DateUtil.format(new Date(), "yyyy-MM-dd") + StrUtil.SLASH + UUID.randomUUID() + StrUtil.UNDERLINE + originalFilename;
    }

    /**
     * 初始化默认存储桶
     */
   /* @PostConstruct
    public void initDefaultBucket() {
        String defaultBucketName = minioProperties.getBucketName();
        if (bucketExists(defaultBucketName)) {
            log.info("默认存储桶：defaultBucketName已存在");
        } else {
            log.info("创建默认存储桶：defaultBucketName");
            makeBucket(defaultBucketName);
        }
    }*/

    /**
     * 查询分片数据
     *
     * @param bucketName       存储桶
     * @param region           区域
     * @param objectName       对象名
     * @param uploadId         上传ID
     * @param extraHeaders     额外消息头
     * @param extraQueryParams 额外查询参数
     * @return
     */
    public CustomListPartsResponse listMultipart(String bucketName, String region, String objectName, Integer partNumberMarker, String uploadId, Multimap<String, String> extraHeaders, Multimap<String, String> extraQueryParams) throws NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException, ServerException, XmlParserException, ErrorResponseException, InternalException, InvalidResponseException {
        return pearlMinioClient.customListParts(bucketName, region, objectName, minioProperties.getMaxParts(), partNumberMarker, uploadId, extraHeaders, extraQueryParams);
    }

    public void copyFile(String path, String oldReplaceVal, String newReplaceVal) {
        Iterable<io.minio.Result<Item>> results = listObjects(path);
        try {
            for (io.minio.Result<Item> result : results) {
                Item item = result.get();
                String objectName = item.objectName();
                if (item.isDir()) {
                    copyFile(objectName, oldReplaceVal, newReplaceVal);
                } else {
//                执行复制操作
                    String newObject = objectName.replace(oldReplaceVal, newReplaceVal);
                    copyFile(objectName, newObject);
                }
            }
        } catch (Exception e) {
            log.error("minio复制对象异常 {} {}", e.getMessage(), e);
            throw new RuntimeException("备份文件异常");
        }
    }

    /**
     * 复制对象
     *
     * @param oldPath
     * @param newPath
     */
    @SneakyThrows
    public void copyFile(String oldPath, String newPath) {
        pearlMinioClient.copyObject(
                CopyObjectArgs.builder()
                        .bucket(minioProperties.getBucketName())
                        .object(newPath)
                        .source(
                                CopySource.builder()
                                        .bucket(minioProperties.getBucketName())
                                        .object(oldPath)
                                        .build())
                        .build());
    }

    /**
     * @param identifier 唯一标识
     * @param fileName   文件名称
     * @param chunkSize  分片大小
     * @return
     */
    @SneakyThrows
    public CreateMultipartUploadResultVO createMultipartUpload(
            String identifier,
            String fileName,
            Integer chunkSize
    ) {
        String objectName = "temp/" + identifier + "/" + fileName;
        // 1. 根据文件名创建签名
        Map<String, Object> result = new HashMap<>();
        // 2. 获取uploadId
        String contentType = "application/octet-stream";

        HashMultimap<String, String> headers = HashMultimap.create();
        headers.put("Content-Type", contentType);
        CreateMultipartUploadResponse response = this.uploadId(minioProperties.getBucketName(), null, objectName, headers, null);
        String uploadId = response.result().uploadId();
        // 3. 请求Minio 服务，获取每个分块带签名的上传URL
        Map<String, String> reqParams = new HashMap<>();
        reqParams.put("uploadId", uploadId);

        // 4. 循环分块数 从1开始,MinIO 存储服务定义分片索引却是从1开始的
        for (int i = 1; i <= chunkSize; i++) {
            reqParams.put("partNumber", String.valueOf(i));
            String uploadUrl = this.getPresignedObjectUrl(minioProperties.getBucketName(), objectName, reqParams);// 获取URL,主要这里前端上传的时候，要传递二进制流，而不是file
            result.put("chunk_" + (i - 1), uploadUrl); // 添加到集合
        }

        CreateMultipartUploadResultVO createMultipartUploadResultVO = new CreateMultipartUploadResultVO();
        createMultipartUploadResultVO.setUploadId(uploadId);
        createMultipartUploadResultVO.setChunks(result);
        return createMultipartUploadResultVO;
    }


    /**
     * @param fileName 文件全路径名称
     * @param uploadId
     */
    @SneakyThrows
    public void completeMultipartUpload(String fileName, String uploadId) {
        try {
            CustomListPartsResponse partResult = this.listMultipart(minioProperties.getBucketName(),
                    null,
                    fileName,
                    0,
                    uploadId,
                    null,
                    null
            );

            Part[] parts = new Part[partResult.result().partList().size()];
            int partNumber = 1;
            for (Part part : partResult.result().partList()) {
                parts[partNumber - 1] = new Part(partNumber, part.etag());
                partNumber++;
            }
            this.completeMultipartUpload(
                    minioProperties.getBucketName(),
                    null,
                    fileName,
                    uploadId,
                    parts,
                    null,
                    null
            );
        } catch (IllegalArgumentException e) {
            log.error("合并分片文件异常： {} ", e.toString());
            throw new RuntimeException("请检查您的分片文件是否全部上传完成");
        } catch (Exception e) {
            log.error("合并分片文件异常： {} ", e.toString());
        }
    }
}
